#include "njm_camera.hpp"

#include <cassert>
#include <limits>
#include <stdexcept>

namespace njm {

    NjmCamera::NjmCamera()
    {
    }
    NjmCamera::~NjmCamera()
    {
    }

    void NjmCamera::setOrthographicProjection(
        float left, float right, float top, float bottom, float near, float far)
    {
        projectionMat = glm::mat4{1.0f};
        projectionMat[0][0] = 2.f / (right - left);
        projectionMat[1][1] = 2.f / (bottom - top);
        projectionMat[2][2] = 1.f / (far - near);
        projectionMat[3][0] = -(right + left) / (right - left);
        projectionMat[3][1] = -(bottom + top) / (bottom - top);
        projectionMat[3][2] = -near / (far - near);
    }
    void NjmCamera::setPerspectiveProjection(float fovy, float aspect, float near, float far)
    {
        assert(glm::abs(aspect - std::numeric_limits<float>::epsilon()) > 0.0f);
        const float tanHalfFovy = tan(fovy / 2.f);
        projectionMat = glm::mat4{0.0f};
        projectionMat[0][0] = 1.f / (aspect * tanHalfFovy);
        projectionMat[1][1] = 1.f / (tanHalfFovy);
        projectionMat[2][2] = far / (far - near);
        projectionMat[2][3] = 1.f;
        projectionMat[3][2] = -(far * near) / (far - near);
    }

    void NjmCamera::setViewDirection(glm::vec3 position, glm::vec3 direction, glm::vec3 up)
    {
        const glm::vec3 viewDir = glm::normalize(direction);
        const glm::vec3 viewRight = glm::normalize(glm::cross(viewDir, up));
        const glm::vec3 viewUp = glm::normalize(glm::cross(viewRight, viewDir));

        viewMat = glm::mat4{1.f};
        viewMat[0][0] = viewRight.x;
        viewMat[1][0] = viewRight.y;
        viewMat[2][0] = viewRight.z;
        viewMat[0][1] = viewUp.x;
        viewMat[1][1] = viewUp.y;
        viewMat[2][1] = viewUp.z;
        viewMat[0][2] = viewDir.x;
        viewMat[1][2] = viewDir.y;
        viewMat[2][2] = viewDir.z;
        viewMat[3][0] = -glm::dot(viewRight, position);
        viewMat[3][1] = -glm::dot(viewUp, position);
        viewMat[3][2] = -glm::dot(viewDir, position);

        // inverseViewMat = glm::inverse(viewMat);
        // inverseViewMat = glm::transpose(viewMat);
        inverseViewMat = glm::mat4{1.f};
        viewMat[0][0] = viewRight.x;
        viewMat[0][1] = viewRight.y;
        viewMat[0][2] = viewRight.z;
        viewMat[1][0] = viewUp.x;
        viewMat[1][1] = viewUp.y;
        viewMat[1][2] = viewUp.z;
        viewMat[2][0] = viewDir.x;
        viewMat[2][1] = viewDir.y;
        viewMat[2][2] = viewDir.z;
        viewMat[3][0] = position.x;
        viewMat[3][1] = position.y;
        viewMat[3][2] = position.z;
    }

    void NjmCamera::setViewTarget(glm::vec3 position, glm::vec3 target, glm::vec3 up)
    {
        setViewDirection(position, target - position, up);
    }

    void NjmCamera::setViewYXZ(glm::vec3 position, glm::vec3 rotation)
    {
        const float c3 = glm::cos(rotation.z);
        const float s3 = glm::sin(rotation.z);
        const float c2 = glm::cos(rotation.x);
        const float s2 = glm::sin(rotation.x);
        const float c1 = glm::cos(rotation.y);
        const float s1 = glm::sin(rotation.y);
        const glm::vec3 u{(c1 * c3 + s1 * s2 * s3), (c2 * s3), (c1 * s2 * s3 - c3 * s1)};
        const glm::vec3 v{(c3 * s1 * s2 - c1 * s3), (c2 * c3), (c1 * c3 * s2 + s1 * s3)};
        const glm::vec3 w{(c2 * s1), (-s2), (c1 * c2)};
        viewMat = glm::mat4{1.f};
        viewMat[0][0] = u.x;
        viewMat[1][0] = u.y;
        viewMat[2][0] = u.z;
        viewMat[0][1] = v.x;
        viewMat[1][1] = v.y;
        viewMat[2][1] = v.z;
        viewMat[0][2] = w.x;
        viewMat[1][2] = w.y;
        viewMat[2][2] = w.z;
        viewMat[3][0] = -glm::dot(u, position);
        viewMat[3][1] = -glm::dot(v, position);
        viewMat[3][2] = -glm::dot(w, position);

        inverseViewMat = glm::mat4{1.f};
        inverseViewMat[0][0] = u.x;
        inverseViewMat[0][1] = u.y;
        inverseViewMat[0][2] = u.z;
        inverseViewMat[1][0] = v.x;
        inverseViewMat[1][1] = v.y;
        inverseViewMat[1][2] = v.z;
        inverseViewMat[2][0] = w.x;
        inverseViewMat[2][1] = w.y;
        inverseViewMat[2][2] = w.z;
        inverseViewMat[3][0] = position.x;
        inverseViewMat[3][1] = position.y;
        inverseViewMat[3][2] = position.z;
    }

}